chore: keep eslint out of agent worktrees - #75
Conversation
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 5s —— View job Review of PR #75
Overall assessmentSolid, well-scoped infra change. The diff matches what the PR description claims, and the reasoning holds up under inspection:
I left two inline comments on
No correctness, security, or test-adequacy issues found that would block merging. The |
A worktree under .claude/worktrees/ is a full checkout nested in the repo, so a root `eslint .` walked into it — 2983 files across two live agent worktrees, linting half-finished work and failing on code the developer never wrote. Scoped the ignore to `.claude/worktrees/` rather than all of `.claude/`, so anything else we put there is still checked. Verified the blast radius is eslint alone: prettier's globs don't descend into dot-directories, and typecheck runs per-package through turbo rather than from the root. The skill said to put worktrees in a sibling directory, which the harness doesn't do and which leaves orphaned checkouts holding branch locks after a repo is deleted. It now documents .claude/worktrees/, the one-worktree-per-branch constraint, and the ignore requirement for any future repo-root tool that walks the tree. Also settles enableGlobalVirtualStore as rejected rather than pending: experimental, documented as broken for ESM under hoisted dependencies, and this repo is ESM throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
A worktree is a full second checkout, so nesting it at
.claude/worktrees/ means every tool that walks the tree from the root
walks into it. The eslint ignore in the previous commit patched one
tool; this removes the class of problem instead. Worktrees now land in
../skills-worktrees/<id>, which also makes "what worktrees do I have"
a single ls.
Worktree placement turned out to be configurable after all — a
WorktreeCreate hook replaces the default logic entirely and returns the
path it created, with WorktreeRemove as its counterpart. Both scripts
verified end to end against synthetic payloads before being wired up.
The remove hook deliberately refuses to delete a worktree with
uncommitted changes: its failures are logged in debug mode only and
never surface, so silently discarding work would be invisible, whereas
a stale worktree is visible in `git worktree list`.
Note the settings schema requires the nested hooks form; the flat
[{type, command}] shape shown on one docs page does not validate.
The .claude/worktrees ignores stay as a backstop for anything that
bypasses the hooks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
6375bcc to
a94e87a
Compare
Three review findings on the create hook, all real: The id reached both a filesystem path and a git ref unvalidated. It comes from the harness, but a trust assumption is worth enforcing rather than assuming — `../` would escape the sibling directory and a leading `-` could be read as a flag. Now constrained to a simple slug, with `..` rejected outright. The idempotency check tested only that the directory existed, so a stray path from a partial cleanup or interrupted run would be returned as though creation had succeeded, handing the caller a directory git knows nothing about. It now asks `git worktree list --porcelain`, and a path that exists without being a registered worktree fails loudly. Also untangled `>&2 2>/dev/null` on the branch delete in the remove hook, which sent success to stderr and swallowed failures entirely. Both now go to stderr for debug logs; neither is fatal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
Both inline findings fixed in Idempotency check now asks
The cryptic — AI Coding Agent |
A top-level review comment has no thread to resolve, so the skill had to dedupe by scanning existing comments for a reference marker citing the same author and snippet. That is fragile, and it failed in practice — on #75 and again on #77 the summary comment kept reporting as unaddressed after it had been answered, leaving a permanent needs_attention to reason about by hand every pass. A hooray reaction on the original is a machine-readable acknowledgement. fetch_pr_feedback now reads reactions.hooray, marks the item acknowledged, and buckets it as resolved, so a re-run reports zero instead of re-surfacing it. Verified on #77: needs_attention went 2 to 0 with no other change. The reaction step is documented against the PR-scoped comments endpoint specifically. The repo-wide repos/{owner}/{repo}/issues/comments returns every comment in the repository, and selecting from it will eventually react on another PR's comment — I did exactly that here and got the right answer by luck. Items now carry comment_id so the id comes from the feedback data rather than from matching body text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDv57zHq7abms3RReSQw6q
A top-level review comment has no thread to resolve, so the skill had to dedupe by scanning existing comments for a reference marker citing the same author and snippet. That is fragile, and it failed in practice — on #75 and again on #77 the summary comment kept reporting as unaddressed after it had been answered, leaving a permanent needs_attention to reason about by hand every pass. A hooray reaction on the original is a machine-readable acknowledgement. fetch_pr_feedback now reads reactions.hooray, marks the item acknowledged, and buckets it as resolved, so a re-run reports zero instead of re-surfacing it. Verified on #77: needs_attention went 2 to 0 with no other change. The reaction step is documented against the PR-scoped comments endpoint specifically. The repo-wide repos/{owner}/{repo}/issues/comments returns every comment in the repository, and selecting from it will eventually react on another PR's comment — I did exactly that here and got the right answer by luck. Items now carry comment_id so the id comes from the feedback data rather than from matching body text.
A worktree under
.claude/worktrees/is a complete second checkout nested inside the repo, so a rooteslint .walked straight into it. With two agent worktrees live, that was 2,983 extra files — linting whatever an agent had half-written and failing on code the developer never touched. Local-only (CI gets a fresh checkout), but confusing precisely when you are least expecting it.The ignore is scoped to
.claude/worktrees/rather than all of.claude/, so any JS we put elsewhere under.claude/or.agents/stays checked.I checked the rest of the toolchain rather than assuming eslint was representative: prettier's globs do not descend into dot-directories, and
tscruns per-package through turbo instead of from the repo root. Neither needs an ignore. That is recorded in the skill so the next person does not re-test it — and so anyone adding a repo-root tool that walks the tree knows to give it the same treatment.Then we went further and moved the worktrees out of the repo entirely. Placement turns out to be configurable: a
WorktreeCreatehook replaces the default logic and returns the path it created, withWorktreeRemoveas its counterpart. Agent and hand-made worktrees now both land in../skills-worktrees/<id>, so no repo-root tool can reach them — the class of problem is gone rather than patched per tool. It also makes "what worktrees do I have" a singlels.The ignore added above stays as a backstop for anything that bypasses the hooks; the cost of a stale ignore line is a smell, the cost of being wrong is silent lint failures.
Two notes for whoever touches this next. The settings schema requires the nested hooks form (
[{ "hooks": [{ type, command }] }]) — the flat shape shown on one docs page does not validate, which is worth knowing before hand-editing. And the remove hook deliberately refuses to delete a worktree with uncommitted changes:WorktreeRemovefailures are logged in debug mode only and never surface, so silently discarding work would be invisible, whereas a stale worktree is visible ingit worktree list.Both hooks were verified end to end against synthetic stdin payloads — created, returned the sibling path, removed, cleaned up the branch and parent directory — before being wired into settings. The two live agent worktrees were migrated with
git worktree move, which rewrites the.gitpointers and carriesnode_modulesalong, so no reinstall was needed.The
worktrees-pnpmskill also had two things wrong, both now fixed:git worktree moverather thanmv.enableGlobalVirtualStoreas an open question to evaluate. It is now recorded as rejected: experimental, documented by pnpm as not working with ESM under hoisted dependencies, and this repo is ESM throughout. The skill says not to re-propose it, and explains that disk is not the reason to avoid a worktree anyway — the pnpm store shares package content by copy-on-write on the same volume, sodu's ~370 MB is apparent rather than incremental.Fixes nothing tracked; found while running two agents in parallel worktrees.